const { ccclass, property, requireComponent } = cc._decorator;

export interface ListViewDelegate<T> {
    items(): T[];
    reuse?(itemNode: cc.Node, item: T): void;
    unuse?(itemNode: cc.Node): void;
}

const itemIndex = new Set<number>();
const vec3 = cc.v3();

@ccclass
@requireComponent(cc.ScrollView)
export default class ListView extends cc.Component {

    @property(cc.Node)
    tmpItem: cc.Node = null;

    @property
    spaceY = 0;

    scrollView: cc.ScrollView = null;
    itemHeight: number = 0;
    itemPool: cc.NodePool = new cc.NodePool();
    dataSource: any[] = [];
    visibleHeight: number = 0;
    spawnCount: number = 0;
    visibleNodes: Map<number, cc.Node> = new Map<number, cc.Node>();
    delegate: ListViewDelegate<any> = { items: () => [] };

    lastY: number = Number.MIN_SAFE_INTEGER;

    onLoad() {
        this.scrollView = this.getComponent(cc.ScrollView);
        this.itemHeight = this.tmpItem.height;
        this.itemPool.put(this.tmpItem);
        this.visibleHeight = this.node.height;
        this.spawnCount = Math.round(this.visibleHeight / this.itemHeight) + 1;
        this.ensure(this.spawnCount);
    }

    start() {
    }

    setDelegate<T>(delegate: ListViewDelegate<T>) {
        this.delegate = delegate;
    }

    reload() {
        this.dataSource = this.delegate.items();
        const totalHeight = this.itemHeight * this.dataSource.length + (this.spaceY * Math.max(0, this.dataSource.length - 1));
        this.scrollView.content.height = totalHeight;
        const children = this.scrollView.content.children.slice();
        children.forEach(c => {
            this.delegate.unuse && this.delegate.unuse(c);
            this.itemPool.put(c);
        });
        this.visibleNodes.clear();
        this.scrollView.stopAutoScroll();
        this.scrollView.scrollToTop(0, false);
        this.lastY = Number.MIN_SAFE_INTEGER;
        cc.log('ListView reload totalCount:', this.dataSource.length);
    }

    ensure(count: number) {
        while (this.itemPool.size() < count) {
            this.itemPool.put(cc.instantiate(this.tmpItem));
        }
    }

    lateUpdate() {
        const y = Math.floor(this.scrollView.getScrollOffset().y);
        if (this.lastY != y) {
            this.lastY = y;
            this.getVisibleItemIndex(y);
            this.visibleNodes.forEach((node, idx) => {
                if (!itemIndex.has(idx)) {
                    this.delegate.unuse && this.delegate.unuse(node);
                    this.itemPool.put(node);
                    this.visibleNodes.delete(idx);
                }
            });
            itemIndex.forEach((idx) => {
                if (!this.visibleNodes.has(idx)) {
                    this.ensure(1);
                    const node = this.itemPool.get();
                    node.getPosition(vec3);
                    vec3.y = (-idx - 0.5) * this.itemHeight - this.spaceY * idx;
                    node.setPosition(vec3);
                    node.parent = this.scrollView.content;
                    this.delegate.reuse && this.delegate.reuse(node, this.dataSource[idx]);
                    this.visibleNodes.set(idx, node);
                }
            });
        }
    }

    getVisibleItemIndex(y: number) {
        let minY = Math.max(0, Math.floor(y / (this.itemHeight + this.spaceY)));
        let maxY = minY == 0 ? this.spawnCount : Math.round((y + this.visibleHeight) / (this.itemHeight + this.spaceY));
        const totalCount = this.dataSource.length;
        maxY = Math.min(maxY, totalCount);
        if (maxY == totalCount) {
            minY = Math.max(0, totalCount - this.spawnCount);
        }
        itemIndex.clear();
        for (let i = minY; i < minY + this.spawnCount && i < totalCount; i++) {
            itemIndex.add(i);
        }
    }
}